﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Management;
using System.Reflection;

namespace MissingLinq.Linq2Management.Context
{
	/// <summary>
	/// Facilitates the querying of Windows management data
	/// </summary>
	public partial class ManagementObjectContext : IDisposable
	{
		/// <summary>
		/// Initializes a <see cref="ManagementObjectContext"/> object
		/// </summary>
		/// <param name="machineName">Optional machine name, or null to use the local machine name</param>
		/// <param name="connectionOptions">Optional connection options, or null to use the default</param>
		public ManagementObjectContext(String machineName = null, ConnectionOptions connectionOptions = null)
		{
			_machineName = machineName ?? Environment.MachineName;
			_connectionOptions = connectionOptions;
		}

		#region IDisposable Members

		/// <see cref="IDisposable.Dispose"/>
		public void Dispose()
		{

		}

		#endregion

		private readonly ConnectionOptions _connectionOptions;
		private readonly String _machineName;
	}

	/// <summary>
	/// Represents a strongly typed set of management data
	/// </summary>
	/// <typeparam name="T">The WMI strongly typed class</typeparam>
	internal class ObjectSet<T> : IOrderedQueryable<T>
	{
		public ObjectSet(ManagementQueryProvider queryProvider, Expression expression = null)
		{
			_provider = queryProvider;
			_expression = expression ?? Expression.Constant(this);
		}

		public ObjectSet(String className, String namespaze, String machineName, ConnectionOptions connectionOptions)
			: this(new ManagementQueryProvider(className, namespaze, machineName, connectionOptions))
		{
			
		}

		#region IEnumerable<T> Members

		public IEnumerator<T> GetEnumerator()
		{
			return ((IEnumerable<T>)_provider.Execute(_expression, typeof(T))).GetEnumerator();
		}

		#endregion

		#region IEnumerable Members

		IEnumerator IEnumerable.GetEnumerator()
		{
			var elementType = TypeSystem.GetElementType(_expression.Type);
			return ((IEnumerable)_provider.Execute(_expression, elementType)).GetEnumerator();
		}

		#endregion

		#region IQueryable Members

		public Type ElementType
		{
			get { return typeof(T); }
		}

		public Expression Expression
		{
			get { return _expression; }
		}

		public IQueryProvider Provider { get { return _provider; } }

		#endregion

		private readonly Expression _expression;
		private readonly ManagementQueryProvider _provider;
	}

	/// <summary>
	/// Executes queries based on management query-able objects using the <see cref="ManagementObjectSearcher"/>
	/// </summary>
	class ManagementQueryProvider : IQueryProvider
	{
		public ManagementQueryProvider(String className, String namespaze, String machineName, ConnectionOptions connectionOptions)
		{
			_className = className;
			_namespaze = namespaze;
			_machineName = machineName;
			_connectionOptions = connectionOptions;
		}

		#region IQueryProvider Members

		public IQueryable<T> CreateQuery<T>(Expression expression)
		{
			return new ObjectSet<T>(this, expression);
		}

		public IQueryable CreateQuery(Expression expression)
		{
			var elementType = TypeSystem.GetElementType(expression.Type);
			try
			{
				return (IQueryable)Activator.CreateInstance(typeof(ObjectSet<>).MakeGenericType(elementType), new object[] { _className, this, expression });
			}
			catch (TargetInvocationException ex)
			{
				throw ex.InnerException;
			}
		}

		public T Execute<T>(Expression expression)
		{
			return (T)Execute(expression, typeof(T));
		}

		object IQueryProvider.Execute(Expression expression)
		{
			var elementType = TypeSystem.GetElementType(expression.Type);
			return Execute(expression, elementType);
		}

		public object Execute(Expression expression, Type elementType)
		{
			String queryString = new QueryTranslator(_className).Translate(expression);
			String path = @"\\" + String.Join(@"\", _machineName, _namespaze);

			var searcher = _connectionOptions != null ? new ManagementObjectSearcher(new ManagementScope(path, _connectionOptions), new ObjectQuery(queryString)) : 
				new ManagementObjectSearcher(path, queryString);

			return Activator.CreateInstance(typeof(ObjectReader<>).MakeGenericType(elementType),
					BindingFlags.Instance | BindingFlags.NonPublic, null,
					new object[] { searcher.Get() }, null);
		}

		#endregion

		private readonly String _className;
		private readonly String _namespaze;
		private readonly ConnectionOptions _connectionOptions;
		private readonly String _machineName;
	}

	internal static class TypeSystem
	{
		internal static Type GetElementType(Type seqType)
		{
			Type ienum = FindIEnumerable(seqType);
			if (ienum == null)
				return seqType;

			return ienum.GetGenericArguments()[0];
		}

		private static Type FindIEnumerable(Type seqType)
		{
			if (seqType == null || seqType == typeof(string))
				return null;

			if (seqType.IsArray)
				return typeof(IEnumerable<>).MakeGenericType(seqType.GetElementType());

			if (seqType.IsGenericType)
			{
				foreach (Type arg in seqType.GetGenericArguments())
				{
					Type ienum = typeof(IEnumerable<>).MakeGenericType(arg);

					if (ienum.IsAssignableFrom(seqType))
						return ienum;
				}
			}

			var ifaces = seqType.GetInterfaces();

			if (ifaces.Length > 0)
			{
				foreach (Type iface in ifaces)
				{
					Type ienum = FindIEnumerable(iface);
					if (ienum != null)
						return ienum;
				}
			}

			if (seqType.BaseType != null && seqType.BaseType != typeof(object))
				return FindIEnumerable(seqType.BaseType);

			return null;
		}
	}

	/// <summary>
	/// Container in which management classes are declared
	/// </summary>
	public abstract class Namespace
	{
		/// <summary>
		/// Initializes a new <see cref="Namespace"/> object
		/// </summary>
		/// <param name="machineName">The name of the machine for which to perform management operations</param>
		/// <param name="connectionOptions">Management connection options</param>
		protected Namespace(String machineName, ConnectionOptions connectionOptions)
		{
			_machineName = machineName;
			_connectionOptions = connectionOptions;
		}

		/// <summary>
		/// The name of the machine for which to perform management operations
		/// </summary>
		protected readonly ConnectionOptions _connectionOptions;
		/// <summary>
		/// Management connection options
		/// </summary>
		protected readonly String _machineName;	
	}
}
